/*
Copyright (c) 2006 Peter Ritchie

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software (the "Software"), to deal in the Software for their own 
(non commercial) personal use subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;

using System.Diagnostics;
using System.Globalization;

namespace MSNSearch
{
	public sealed partial class MainForm : Form
	{
		public MainForm ( )
		{
			InitializeComponent();
		}

		#region Infrastructure
		/// <summary>
		/// State friendlies
		/// </summary>
		enum State
		{
			Idle,
			Running
		}

		/// <summary>
		/// state of the application.  Currently signifies
		/// change of state of the SearchAsync method call.
		/// </summary>
		State state = State.Idle;
		string deleteThreadButtonSingleSelectionText, moveThreadButtonSingleSelectionText;
		string deleteThreadButtonMultiselectionText, moveThreadButtonMultiselectionText;
		int currentOffset = 0;
		int totalResults = 0;

		private void MainForm_Load ( object sender, EventArgs e )
		{
			webService.SearchCompleted += new MSNSearch.MSNSearchWebService.SearchCompletedEventHandler(webService_SearchCompleted);
			resultPagingLabel.Text = String.Empty;
			deleteThreadButtonSingleSelectionText = deleteThreadButton.Text;
			moveThreadButtonSingleSelectionText = moveThreadButton.Text;
			deleteThreadButtonMultiselectionText = global::MSNSearch.Properties.Resources.label_deleteThreadButtonSingleSelectionText;
			moveThreadButtonMultiselectionText = global::MSNSearch.Properties.Resources.label_moveThreadButtonSingleSelectionText;

			UpdateControls();
		}

		#region instance helpers
		private void UpdateControls ( )
		{
			if (this.state == State.Running)
			{
				previousPageButton.Enabled = false;
				nextPageButton.Enabled = false;
				goButton.Enabled = false;
				deleteThreadButton.Enabled = false;
				moveThreadButton.Enabled = false;
				statusLabel.Text = global::MSNSearch.Properties.Resources.status_Running;
			}
			else if (this.state == State.Idle)
			{
				previousPageButton.Enabled = currentOffset != 0;
				nextPageButton.Enabled = (currentOffset + (int)resultCountNumericUpDown.Value) < totalResults;
				goButton.Enabled = searchTextBox.Text.Length > 0;
				;
				deleteThreadButton.Enabled = resultsListView.SelectedIndices.Count > 0;
				moveThreadButton.Enabled = resultsListView.SelectedIndices.Count > 0;
				if (resultsListView.SelectedIndices.Count > 1)
				{
					deleteThreadButton.Text = deleteThreadButtonMultiselectionText;
					moveThreadButton.Text = moveThreadButtonMultiselectionText;
				}
				else
				{
					deleteThreadButton.Text = deleteThreadButtonSingleSelectionText;
					moveThreadButton.Text = moveThreadButtonSingleSelectionText;
				}
				statusLabel.Text = global::MSNSearch.Properties.Resources.status_Idle;
			}
		}
		#endregion instance helpers

		private void MainForm_FormClosing ( object sender, FormClosingEventArgs e )
		{
			// store settings in case they've changed
			global::MSNSearch.Properties.Settings.Default.Last_ResultCount = resultCountNumericUpDown.Value;
			global::MSNSearch.Properties.Settings.Default.Last_Search_Text = searchTextBox.Text;
			global::MSNSearch.Properties.Settings.Default.MSNSearchAppID = appIDTextBox.Text;
		}

		#region Control event handlers where dependant control state may change
		private void searchTextBox_TextChanged ( object sender, EventArgs e )
		{
			UpdateControls();
		}

		private void resultsListView_SelectedIndexChanged ( object sender, EventArgs e )
		{
			UpdateControls();
		}
		#endregion Control event handlers where dependant control state may change

		#region Button click handlers
		private void goButton_Click ( object sender, EventArgs e )
		{
			this.state = State.Running;
			UpdateControls();

			try
			{
				MSNSearchWebService.SearchRequest request = new MSNSearch.MSNSearchWebService.SearchRequest();
				request.AppID = appIDTextBox.Text;
				request.Flags = MSNSearch.MSNSearchWebService.SearchFlags.None;
				request.SafeSearch = MSNSearch.MSNSearchWebService.SafeSearchOptions.Off;
				request.Query = searchTextBox.Text +
					" site:forums.microsoft.com -meta:Search.MSForums.viewtype(\"Forum\") meta:Search.MSForums.SiteID(\"1\")";
				request.CultureInfo = "en-US";
				request.Requests = new MSNSearch.MSNSearchWebService.SourceRequest[1];
				request.Requests[0] = new MSNSearch.MSNSearchWebService.SourceRequest();
				request.Requests[0].Source = MSNSearch.MSNSearchWebService.SourceType.Web;
				request.Requests[0].ResultFields = MSNSearch.MSNSearchWebService.ResultFieldMask.All | MSNSearch.MSNSearchWebService.ResultFieldMask.SearchTagsArray;
				request.Requests[0].Offset = currentOffset;
				request.Requests[0].Count = (int)resultCountNumericUpDown.Value;
				webService.SearchAsync(request);
			}
			catch
			{
				state = State.Idle;
				UpdateControls();

				throw;
			}
		}

		private void deleteThreadButton_Click ( object sender, EventArgs e )
		{
			MessageBox.Show(this, global::MSNSearch.Properties.Resources.general_NotImplementedYet,
				this.Text,	// reuse form's caption
				MessageBoxButtons.OK,
				MessageBoxIcon.Information,
				MessageBoxDefaultButton.Button1,
				RightToLeft == RightToLeft.Yes ? MessageBoxOptions.RtlReading : 0);
		}

		private void moveThreadButton_Click ( object sender, EventArgs e )
		{
			MessageBox.Show(this,
				global::MSNSearch.Properties.Resources.general_NotImplementedYet,
				this.Text,	// reuse form's caption
				MessageBoxButtons.OK,
				MessageBoxIcon.Information,
				MessageBoxDefaultButton.Button1,
				RightToLeft == RightToLeft.Yes ? MessageBoxOptions.RtlReading : 0);
		}
		#endregion Button click handlers
		#endregion Infrastructure

		#region Value-added
		MSNSearchWebService.MSNSearchService webService = new MSNSearch.MSNSearchWebService.MSNSearchService();

		void webService_SearchCompleted ( object sender, MSNSearch.MSNSearchWebService.SearchCompletedEventArgs e )
		{
			// clear any existing results.
			resultsListView.Items.Clear();

			try
			{
				// for each result parse the tags and display them in the listview.
				foreach (MSNSearchWebService.Result result in e.Result.Responses[0].Results)
				{
					// start with an unnamed item, with blank subitems.
					ListViewItem item = new ListViewItem();
					item.SubItems.Add(new ListViewItem.ListViewSubItem());
					item.SubItems.Add(new ListViewItem.ListViewSubItem());
					item.SubItems.Add(new ListViewItem.ListViewSubItem());
					item.SubItems.Add(new ListViewItem.ListViewSubItem());
					item.SubItems.Add(new ListViewItem.ListViewSubItem());

					// TODO: problem, the tags are comma-delimited by the MSN Search Web Service,
					// but, it doesn't escape commas contained within the tags; making commas
					// as delimiters pointless.
					// There's no way to tell if a comma is part of the tag or a delimiter
					// so, the following  code will choke every now and then.
					if (result.SearchTagsArray != null)
					{
						foreach (MSNSearchWebService.SearchTag tag in result.SearchTagsArray)
						{
							string text = tag.Value;
							if (text.Length > 2 && text[0] == '"' && text[text.Length - 1] == '"')
							{
								text = text.Substring(1, tag.Value.Length - 2);
							}
							// check for tags we know how to deal with
							if (tag.Name.StartsWith("search.msforums.forumname"))
							{
								if (String.IsNullOrEmpty(item.SubItems[1].Text))
								{
									item.SubItems[1].Text = text;
								}
							}
							else if (tag.Name.StartsWith("search.msforums.threadname"))
							{
								if (String.IsNullOrEmpty(item.Text))
								{
									item.Text = text;
								}
							}
							else if (tag.Name.StartsWith("search.msforums.threadstarterusername"))
							{
								if (String.IsNullOrEmpty(item.SubItems[5].Text))
								{
									item.SubItems[5].Text = text;
								}
							}
							else if (tag.Name.StartsWith("search.msforums.helpfulthread"))
							{
								if (String.IsNullOrEmpty(item.SubItems[3].Text))
								{
									int isHelpfulNumber = 0;
									if (int.TryParse(text, out isHelpfulNumber))
									{
										Boolean isHelpful = isHelpfulNumber != 0;
										item.SubItems[3].Text = isHelpful.ToString();
									}
								}
							}
							else if (tag.Name.StartsWith("search.msforums.isanswered"))
							{
								if (String.IsNullOrEmpty(item.SubItems[4].Text))
								{
									int isAnsweredNumber = 0;
									if (int.TryParse(text, out isAnsweredNumber))
									{
										Boolean isAnswered = isAnsweredNumber != 0;
										item.SubItems[4].Text = isAnswered.ToString();
									}
								}
							}
							else if (tag.Name.StartsWith("search.msforums.isquestion"))
							{
								if (String.IsNullOrEmpty(item.SubItems[2].Text))
								{
									int isQustionNumber = 0;
									if (int.TryParse(text, out isQustionNumber))
									{
										Boolean isQustion = isQustionNumber != 0;
										item.SubItems[2].Text = isQustion.ToString();
									}
								}
							}
						}
					}
					else
					{
						String[] tags = result.SearchTags.Split(new char[] { ',' });
						foreach (string tag in tags)
						{
							int startIndex = tag.IndexOf('"');
							int endIndex = tag.IndexOf('"', startIndex + 1);

							if (startIndex != -1 && endIndex != -1)
							{
								string text = tag.Substring(startIndex + 1, endIndex - startIndex - 1);

								// check for tags we know how to deal with
								if (tag.StartsWith(" search.msforums.forumname:("))
								{
									item.SubItems[1].Text = text;
								}
								else if (tag.StartsWith(" search.msforums.threadname:("))
								{
									item.Text = text;
								}
								else if (tag.StartsWith(" search.msforums.threadstarterusername:("))
								{
									item.SubItems[5].Text = text;
								}
								else if (tag.StartsWith(" search.msforums.helpfulthread:("))
								{
									int isHelpfulNumber = 0;
									if (int.TryParse(text, out isHelpfulNumber))
									{
										Boolean isHelpful = isHelpfulNumber != 0;
										item.SubItems[3].Text = isHelpful.ToString();
									}
								}
								else if (tag.StartsWith(" search.msforums.isanswered:("))
								{
									int isAnsweredNumber = 0;
									if (int.TryParse(text, out isAnsweredNumber))
									{
										Boolean isAnswered = isAnsweredNumber != 0;
										item.SubItems[4].Text = isAnswered.ToString();
									}
								}
								else if (tag.StartsWith(" search.msforums.isquestion:("))
								{
									int isQustionNumber = 0;
									if (int.TryParse(text, out isQustionNumber))
									{
										Boolean isQustion = isQustionNumber != 0;
										item.SubItems[2].Text = isQustion.ToString();
									}
								}
							}
							else
							{
								// if invalid tag just display the title.
								item.Text = result.Title;
							}
						}
					}
					if (String.IsNullOrEmpty(item.Text))
					{
						item.Text = global::MSNSearch.Properties.Resources.result_UnnamedPlaceholder;
					}
					item.Tag = result.Url;
					this.resultsListView.Items.Add(item);
				}
				totalResults = e.Result.Responses[0].Total;
				currentOffset = e.Result.Responses[0].Offset;
				resultPagingLabel.Text = String.Format(CultureInfo.InvariantCulture,
					global::MSNSearch.Properties.Resources.label_PageOrdinal,
					(currentOffset + 1),
					(currentOffset + 1) + (int)resultCountNumericUpDown.Value,
					totalResults);
			}
			catch (System.InvalidOperationException invalidOperationException)
			{
				// InvalidOperationException int this context generally means there was an
				// exception in another process or another AppDomain, so it won't affect
				// our domain. get the exceptions message to display to the user, if any.
				String message = invalidOperationException.Message;
				if (invalidOperationException.InnerException != null)
				{
					message = invalidOperationException.InnerException.Message;
				}

				// just show the user the message and swallow it
				MessageBox.Show(this,
					String.Format(CultureInfo.CurrentCulture,
						global::MSNSearch.Properties.Resources.message_SearchFailed,
						message),
					this.Text,	// reuse form's caption
					MessageBoxButtons.OK,
					MessageBoxIcon.Information,
					MessageBoxDefaultButton.Button1,
					RightToLeft == RightToLeft.Yes ? MessageBoxOptions.RtlReading : 0);
			}
			finally
			{
				// done here instead of the top of the method incase something between
				// here and there takes a bit of time; we don't want the controls to
				// becomes active until the last possible moment.
				state = State.Idle;
				UpdateControls();
			}
		}

		/// <summary>
		/// BackgroundWorker.DoWork event handler.
		/// Just spawn a process to handle the URL based on the system's file association.
		/// </summary>
		private void spawnBrowserBackgroundWorker_DoWork ( object sender, DoWorkEventArgs e )
		{
			String url = e.Argument as String;
			if (url != null)
			{
				Process process = new Process();
				process.StartInfo.FileName = "rundll32.exe";
				process.StartInfo.Arguments = "url.dll,FileProtocolHandler " + url;
				process.StartInfo.UseShellExecute = true;
				process.Start();
			}
		}

		/// <summary>
		/// Spawn a browser when an item is double clicked.
		/// </summary>
		/// <param name="sender">The source of the event. In this case the <see cref="ListView"/> object.</param>
		/// <param name="e">An <see cref="EventARgs"/> object that contains no event data.</param>
		private void resultsListView_ItemActivate ( object sender, EventArgs e )
		{
			ListView listView = sender as ListView;
			if (listView != null)
			{
				ListViewItem item = listView.SelectedItems[0];
				String url = item.Tag as String;
				if (!String.IsNullOrEmpty(url))
				{
					// Asynchronously spawn a browser as this may take
					// several seconds.
					// Use BackgroundWorker to avoid queueing with the thread pool
					// and avoid delay with Control.BeginInvoke.
					spawnBrowserBackgroundWorker.RunWorkerAsync(url);
				}
				else
				{
					// TODO: Better feedback to the user.
					SafeNativeMethods.MessageBeep(SafeNativeMethods.BeepType.Standard);
				}
			}
		}
		#endregion Value-added

		private void nextPageButton_Click ( object sender, EventArgs e )
		{
			currentOffset += (int)this.resultCountNumericUpDown.Value;
			goButton_Click(goButton, EventArgs.Empty);
		}

		private void previousPageButton_Click ( object sender, EventArgs e )
		{
			currentOffset -= (int)this.resultCountNumericUpDown.Value;
			if (currentOffset < 0)
				currentOffset = 0;
			goButton_Click(goButton, EventArgs.Empty);
		}
	}
}